﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace Revolio
{
    /// <summary>
    /// Maintains a collection of Revolio books.
    /// </summary>
    [XmlRoot("BookStore")]
    public class BookStore
    {
        private static readonly XmlSerializer serializer =
            new XmlSerializer(typeof(BookStore));

        private List<BookTag> tags = new List<BookTag>();
        private List<BookEntry> books = new List<BookEntry>();

        /// <summary>
        /// Gets a collection of the books available in this store.
        /// </summary>
        [XmlElement("Book")]
        public List<BookEntry> Books
        {
            get { return books; }
        }

        [XmlElement("Tag")]
        public List<BookTag> Tags
        {
            get { return tags; }
        }

        /// <summary>
        /// Creates a tag of the given name. If a tag with that name already
        /// exists, this method does nothing.
        /// </summary>
        /// <param name="name">Name of the tag to create.</param>
        /// <returns>The tag.</returns>
        public BookTag CreateTag(string name)
        {
            BookTag tag = GetTag(name);
            if (tag == null)
            {
                tag = new BookTag();
                tag.Name = name;
                tags.Add(tag);
            }
            return tag;
        }

        public BookTag GetTag(string name)
        {
            return tags.Find((x) => x.Name == name);
        }

        /// <summary>
        /// Deserializes a BookStore object from a stream in XML format.
        /// </summary>
        /// <param name="stream">The stream to read from.</param>
        /// <returns>The deserialized BookStore object.</returns>
        public static BookStore Load(Stream stream)
        {
            return (BookStore)serializer.Deserialize(stream);
        }

        /// <summary>
        /// Serializes this BookStore object to a stream in XML format.
        /// </summary>
        /// <param name="stream">The stream to write to.</param>
        public void Save(Stream stream)
        {
            serializer.Serialize(stream, this);
        }
    }

    /// <summary>
    /// Contains information about a Revolio book.
    /// </summary>
    public class BookEntry
    {
        [XmlAttribute]
        public int Id { get; set; }

        [XmlAttribute]
        public string Title { get; set; }

        [XmlAttribute]
        public string SubTitle { get; set; }

        [XmlAttribute]
        public int PageCount { get; set; }
    }

    /// <summary>
    /// Represents a tag used to classify books in the store.
    /// </summary>
    public class BookTag
    {
        private List<int> bookIds = new List<int>();

        [XmlAttribute]
        public string Name { get; set; }

        [XmlIgnore]
        public List<int> BookIds
        {
            get { return bookIds; }
        }

        /// <summary>
        /// Gets or sets the book id list in string format: 1,4-5,6,7,8-20,89
        /// </summary>
        [XmlText]
        public string BookIdsToSerialize
        {
            get
            {
                StringBuilder sb = new StringBuilder();
                bookIds.Sort();
                int n = bookIds.Count;
                for (int i = 0; i < n; )
                {
                    int j = i + 1;
                    while (j < n && bookIds[j] == bookIds[j - 1] + 1)
                        j++;
                    if (sb.Length > 0)
                        sb.Append(',');
                    EncodeRange(sb, bookIds[i], bookIds[j - 1]);
                    i = j;
                }
                return sb.ToString();
            }
            set
            {
                string s = value;
                bookIds.Clear();
                for (int k = 0; k < s.Length; )
                {
                    int k2 = s.IndexOf(',', k);
                    if (k2 == -1)
                        k2 = s.Length;
                    if (k2 > k)
                    {
                        int from, to;
                        DecodeRange(s.Substring(k, k2 - k), out from, out to);
                        for (int id = from; id <= to; id++)
                            bookIds.Add(id);
                    }
                    k = k2 + 1;
                }
            }
        }

        /// <summary>
        /// Encodes the range [a, b] as a string. Example:
        /// [123, 123] => 123
        /// [456, 457] => 456-7
        /// [789, 889] => 789-889
        /// [12345, 12356] => 12345-56
        /// </summary>
        /// <param name="sb">StringBuilder to append the result to.</param>
        /// <param name="from">Start of the range (inclusive).</param>
        /// <param name="to">End of the range (inclusive).</param>
        private static void EncodeRange(StringBuilder sb, int from, int to)
        {
            if (from < 0)
                throw new ArgumentOutOfRangeException("from");
            if (to < from)
                throw new ArgumentOutOfRangeException("to");

            sb.Append(from);
            if (from == to)
                return;

            // Find the number of common leading digits in 'from' and 'to'.
            const int max_k = 1000000000;
            int k = 10;
            while (k < max_k && from / k != to / k)
                k *= 10;

            sb.Append('-');
            sb.Append((k == max_k) ? to : to % k);
        }

        /// <summary>
        /// Decodes a string into range [a, b]. Example:
        /// 123      => [123, 123]
        /// 456-7    => [456, 457]
        /// 789-889  => [789, 889]
        /// 12345-56 => [12345, 12356]
        /// </summary>
        /// <param name="sb">StringBuilder to append the result to.</param>
        /// <param name="from">Start of the range (inclusive).</param>
        /// <param name="to">End of the range (inclusive).</param>
        private static void DecodeRange(string s, out int from, out int to)
        {
            if (s == null)
                throw new ArgumentNullException("s");

            int k = s.IndexOf('-');
            if (k == -1)
            {
                to = from = Int32.Parse(s);
                return;
            }

            from = Int32.Parse(s.Substring(0, k));
            to = Int32.Parse(s.Substring(k + 1));

            // Find the number of digits in 'to'.
            const int max_d = 1000000000;
            int d = 1;
            while (d < max_d && to >= d)
                d *= 10;
            to += from / d * d;
        }

        public override string ToString()
        {
            return string.Format("{0}", this.Name);
        }
    }
}
